//***************************************************************************
// Project:   MDT 2010 Wizard Editor
// File:      MainForm.cs
// Purpose:   The main form for editing a Microsoft Deployment Toolkit 
//            wizard XML file.
//
// Version:   2.0.0
//
// Change history:
// 1.0.1  mniehaus  06/25/2008  Cleaned up and commented code for external
//                              release.
// 2.0.0  mniehaus  10/11/2009  Updated for MDT 2010.  Restructured the code
//                              to make it more modular.  Added logic
//                              to display variable values.
// 2.0.2  mniehaus  10/21/2009  Fixed preview logic to display extended
//                              characters (save pane as UTF-8).
//
//***************************************************************************

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Xml;

namespace WizardEditor
{
    /// <summary>
    /// The main form displays the primary user interface.
    /// </summary>
    public partial class MainForm : Form
    {
        #region Instance variables

        String wizardDefinitionFile = "";
        String definitionDirectory = "";

        XmlDocument wizard = new XmlDocument();

        Boolean isDataReady;
        Boolean changed;

        #endregion Intsance variables

        #region Constructor

        /// <summary>
        /// Constructor for the main form
        /// </summary>
        public MainForm()
        {
            InitializeComponent();
            listDetail.DisplayMember = "Name";
        }

        #endregion Constructor

        #region Event handlers

        /// <summary>
        /// The MainForm_Load event handler is called after the form has been loaded 
        /// and initialized.  It can then tweak the UI controls as required.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MainForm_Load(object sender, EventArgs e)
        {
            ConfigureButtons();
            panelFile.Visible = false;
            panelText.Visible = false;
        }

        /// <summary>
        /// The paneTree_AfterSelect method is called each time a new node is 
        /// selected in the left-hand tree view.  It is responsible for updating
        /// the right-hand panes based on the current selection.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void paneTree_AfterSelect(object sender, TreeViewEventArgs e)
        {
            LoadCode();
            LoadPreview();

            // Hide the detail panels after the pane is changed
            panelFile.Visible = false;
            panelText.Visible = false;
        }

        /// <summary>
        /// The tabDetails_Selected method is called when any of the tabs is activated.
        /// If the selected tab is the preview tab, it needs to make sure the preview
        /// contains the latest HTML code for the pane.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void tabDetails_Selected(object sender, System.Windows.Forms.TabControlEventArgs e)
        {
            if (e.TabPage == tabPreview)
            {
                LoadPreview();
            }
        }

        /// <summary>
        /// The listDetail_SelectedIndexChanged event handler is called when a 
        /// different item is selected in the list on the "Settings" pane.  It
        /// updates the right side of the "Settings" pane based on the current
        /// selection.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void listDetail_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (listDetail.SelectedItem == null)
            {
                isDataReady = false;

                panelFile.Visible = false;
                panelText.Visible = false;
                txtDetail.Text = "";

                isDataReady = true;

                return;
            }

            XmlNode selectedNode = (XmlNode)listDetail.SelectedItem;
            XmlNode paneNode = (XmlNode)paneTree.SelectedNode.Tag;

            isDataReady = false;

            if (paneNode.Name == "Global" && (selectedNode.Name == "Initialization" || selectedNode.Name == "Validation"))
            {
                panelFile.Visible = true;
                panelText.Visible = false;
                txtGlobalFile.Text = selectedNode.InnerText;
            }
            else if (selectedNode.Name.StartsWith("Button", StringComparison.OrdinalIgnoreCase))
            {
                panelFile.Visible = false;
                panelText.Visible = true;
                txtDetail.Text = selectedNode.InnerXml;
            }
            else
            {
                panelFile.Visible = false;
                panelText.Visible = true;
                txtDetail.Text = selectedNode.InnerText;
            }

            isDataReady = true;
        }

        /// <summary>
        /// Add a new item to the list on the "Settings" pane.  The ChooseType
        /// form is used to specify the details of the item to add.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAdd_Click(object sender, EventArgs e)
        {
            XmlNode selectedNode = (XmlNode)paneTree.SelectedNode.Tag;
            ChooseType newEntry = new ChooseType(selectedNode.Name);
            if (newEntry.ShowDialog() == DialogResult.OK)
            {
                XmlNode newNode = wizard.CreateElement(newEntry.SelectedType);
                if (newNode.Name.StartsWith("Button", StringComparison.OrdinalIgnoreCase))
                    newNode.InnerXml = @"<Label><![CDATA[<button></button>]]></Label>";
                else
                    newNode.InnerXml = @"<![CDATA[]]>";
                selectedNode.AppendChild(newNode);
                listDetail.Items.Add(newNode);
                changed = true;
            }
        }

        /// <summary>
        /// Remove an item from the list on the "Settings" pane.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnRemove_Click(object sender, EventArgs e)
        {
            if (listDetail.SelectedItem == null)
                return;

            // TODO: Ask for confirmation

            XmlNode selectedNode = (XmlNode)listDetail.SelectedItem;
            selectedNode.ParentNode.RemoveChild(selectedNode);
            listDetail.Items.Remove(listDetail.SelectedItem);
            txtDetail.Text = "";
            if (listDetail.Items.Count > 0)
                listDetail.SelectedItem = 0;
            changed = true;
        }

        /// <summary>
        /// The btnAddPane_Click event handler is called when the user
        /// clicks the "Add" button under the Panes list.  This method
        /// uses the ChooseID form to ask for the details needed for the
        /// new pane, then adds the new pane to the XML file and to the
        /// list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAddPane_Click(object sender, EventArgs e)
        {
            XmlNode selectedNode = (XmlNode)paneTree.SelectedNode.Tag;

            // Prompt for the pane ID

            ChooseId newPaneID = new ChooseId();
            if (newPaneID.ShowDialog() != DialogResult.OK)
                return;

            // Add the new pane to the XML

            XmlNode newPane = wizard.CreateElement("Pane");

            XmlAttribute newAttribute = wizard.CreateAttribute("id");
            newAttribute.Value = newPaneID.PaneId;
            newPane.Attributes.SetNamedItem(newAttribute);

            XmlNode newBody = wizard.CreateElement("Body");
            newBody.InnerXml = @"<![CDATA[<h1>Hello world!</h1>]]>";
            newPane.AppendChild(newBody);

            if (selectedNode != null)
                wizard.DocumentElement.InsertAfter(newPane, selectedNode);
            else
                wizard.DocumentElement.AppendChild(newPane);


            // Add the new pane to the tree

            TreeNode newNode = new TreeNode(newPaneID.PaneId);
            newNode.Tag = newPane;

            if (selectedNode == null)
                paneTree.Nodes[0].Nodes.Add(newNode);  // Add at bottom
            else if (selectedNode.Name == "Global")
                paneTree.Nodes[0].Nodes.Insert(0, newNode);
            else
                paneTree.Nodes[0].Nodes.Insert(paneTree.SelectedNode.Index + 1, newNode);

            changed = true;
        }

        /// <summary>
        /// Remove an existing wizard pane from the XML and from the list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnRemovePane_Click(object sender, EventArgs e)
        {
            // Is a pane currently selected?  If not, return.
            if (paneTree.SelectedNode == null)
                return;

            // TODO: Ask for confirmation

            XmlNode selectedNode = (XmlNode)paneTree.SelectedNode.Tag;
            selectedNode.ParentNode.RemoveChild(selectedNode);
            paneTree.Nodes.Remove(paneTree.SelectedNode);
            if (paneTree.Nodes.Count > 0)
                paneTree.SelectedNode = paneTree.Nodes[0];

            changed = true;
        }

        /// <summary>
        /// The btnEdit_Click method will open the specified script file in Notepad for the user to edit.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnEdit_Click(object sender, EventArgs e)
        {
            // File specified?  If not, return.

            if (String.IsNullOrEmpty(txtGlobalFile.Text.Trim()))
                return;

            // Start a process to run Notepad.exe, passing it the name of the file to open.

            Process wizProcess = new Process();
            ProcessStartInfo startInfo = new ProcessStartInfo("notepad.exe", "\"" + definitionDirectory + @"\" + txtGlobalFile.Text.Trim() + "\"");
            startInfo.WindowStyle = ProcessWindowStyle.Normal;
            startInfo.UseShellExecute = false;
            wizProcess.StartInfo = startInfo;
            wizProcess.Start();
        }

        /// <summary>
        /// The htmlCode_TextChanged event is fired any time the user changes the text in the HTML textbox.
        /// As a result of this event, we need to save the text back into the appropriate XML node.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void htmlCode_TextChanged(object sender, EventArgs e)
        {
            XmlNode selectedPane = (XmlNode)paneTree.SelectedNode.Tag;

            // Pane selected?  If not, return.
            if (selectedPane.Name == null)
                return;

            // Global node selected?  If so, it has no HTML so return.
            if (selectedPane.Name == "Global")
                return;

            // Update the Body node with the current HTML text.
            selectedPane.SelectSingleNode("Body").ChildNodes[0].InnerText = htmlCode.Text;

            if (isDataReady)
                changed = true;
        }

        /// <summary>
        /// The txtGlobalFile_TextChanged event is fired when the name of the script is changed.
        /// As a result of the change, we need to save the new file name back into the XML node.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void txtGlobalFile_TextChanged(object sender, EventArgs e)
        {
            XmlNode selectedNode = (XmlNode)listDetail.SelectedItem;
            if (selectedNode == null)
                return;

            // Update the current XML node with the new text from the form.
            selectedNode.InnerText = txtGlobalFile.Text;
            if (isDataReady)
                changed = true;
        }

        /// <summary>
        /// The txtDetail_TextChanged event is fired when the detail for a button or
        /// a custom statement is changed.  As a result of the change, the updated
        /// text needs to be saved back into the XML node.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void txtDetail_TextChanged(object sender, EventArgs e)
        {
            XmlNode selectedNode = (XmlNode)listDetail.SelectedItem;
            if (selectedNode == null)
                return;

            // Update the appropriate node.  If it is a button, we need
            // to update the inner XML, otherwise we change the inner text.
            if (selectedNode.Name.StartsWith("Button", StringComparison.OrdinalIgnoreCase))
                selectedNode.InnerXml = txtDetail.Text;
            else
                selectedNode.InnerXml = wizard.CreateCDataSection(txtDetail.Text).OuterXml;
            if (isDataReady)
                changed = true;
        }

        private void paneTree_ItemDrag(object sender, ItemDragEventArgs e)
        {
            DoDragDrop(e.Item, DragDropEffects.Copy);
        }

        #region Menu event handlers

        /// <summary>
        /// The openToolStripMenuItem_Click event handler menut is called when the user
        /// chooses the "Open" menu item.  It presents a file dialog to allow the
        /// user to select an XML file, then validates that the XML file is indeed a 
        /// wizard XML file.  After validation, it reads the XML file and populates the
        /// left-hand pane.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // Select the file
            if (wizardFileDialog.ShowDialog() != DialogResult.OK)
                return;

            // Load the file
            wizard.Load(wizardFileDialog.FileName.Trim());
            if (wizard.SelectNodes("//Pane").Count == 0)
            {
                MessageBox.Show("Invalid definition XML file selected, no panes defined.", "Invalid File", 
                    MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly);
                wizardDefinitionFile = "";
                paneTree.Nodes.Clear();
                ConfigureButtons();
                return;
            }

            changed = false;
            isDataReady = false;
            wizardDefinitionFile = wizardFileDialog.FileName.Trim();

            // Set the definition directory.  It needs to contain all the other files and must be writable, as we'll store the 
            // temporary panes there.
            FileInfo definitionInfo = new FileInfo(wizardDefinitionFile);
            definitionDirectory = definitionInfo.DirectoryName;

            // Clear the tree contents
            paneTree.Nodes.Clear();

            // Add a new global node
            TreeNode globalNode = new TreeNode("Global");
            globalNode.Tag = wizard.SelectSingleNode("//Global");
            paneTree.Nodes.Add(globalNode);

            // Make the panes child nodes
            foreach (XmlNode pane in wizard.SelectNodes("//Pane"))
            {
                TreeNode newNode = new TreeNode(pane.Attributes["id"].Value);
                newNode.Tag = pane;
                globalNode.Nodes.Add(newNode);
            }

            // Select the global node
            paneTree.SelectedNode = paneTree.Nodes[0];
            paneTree.ExpandAll();

            ConfigureButtons();
            isDataReady = true;
        }

        /// <summary>
        /// The saveToolStripMenuItem_Click method saves the currently-open definition file.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void saveToolStripMenuItem_Click(object sender, EventArgs e)
        {
            wizard.Save(wizardDefinitionFile);
            changed = false;
        }

        /// <summary>
        /// The saveAsToolStripMenuItem_Click method saves the currently-open definition file with a new name.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            saveAsDialog.InitialDirectory = definitionDirectory;
            if (saveAsDialog.ShowDialog() != DialogResult.OK)
                return;

            wizard.Save(saveAsDialog.FileName);
            wizardDefinitionFile = saveAsDialog.FileName;
            definitionDirectory = Directory.GetParent(wizardDefinitionFile).FullName;
        }

        /// <summary>
        /// The exitToolStripMenuItem_Click event handler method is called when the user chooses the "Exit"
        /// menu item.  It just closes the form (and therefore, the application).
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // TODO: Prompt if there are changes
            this.Close();
        }

        private void testToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // XML file currently open?  If not, return.
            if (String.IsNullOrEmpty(wizardDefinitionFile.Trim()))
                return;

            // Tell the user if there are pending changes that need to be saved first.
            if (changed)
            {
                MessageBox.Show("Please save the changes that were made to the wizard definition XML file before testing the wizard.", "Unable to test until saved",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly);
                return;
            }

            // Display a form with the variable values, allowing the user to override anything they want.
            Variables variableForm = new Variables();
            variableForm.DefinitionFile = wizardDefinitionFile.Trim();
            variableForm.ShowDialog();
        }

        void paneTree_NodeMouseClick(object sender, System.Windows.Forms.TreeNodeMouseClickEventArgs e)
        {
            if (e.Button == MouseButtons.Right)
            {
                paneTree.SelectedNode = e.Node;
            }
        }

        private void copyToolStripMenuItem_Click(object sender, EventArgs e)
        {
            // Nothing to copy if no node is selected
            if (paneTree.SelectedNode == null)
                return;

            // Nothing to copy if the root node is selected
            if (paneTree.SelectedNode == paneTree.Nodes[0])
            {
                System.Console.Beep();
                return;
            }

            // Put the XML on the clipboard
            Clipboard.SetData("WizardPane", ((XmlNode)paneTree.SelectedNode.Tag).OuterXml);
        }

        private void pasteToolStripMenuItem_Click(object sender, EventArgs e)
        {
            String nodeXML;

            // If the clipboard doesn't contain a wizard pane, then there's nothing to paste so return
            if (Clipboard.ContainsData("WizardPane"))
            {
                // Get the XML string for the pane
                nodeXML = (String)Clipboard.GetData("WizardPane");
            }
            else if (Clipboard.ContainsData("Text"))
            {
                String clipboardText = Clipboard.GetText(TextDataFormat.UnicodeText).Trim();
                if (clipboardText.StartsWith("<Pane", StringComparison.OrdinalIgnoreCase) && clipboardText.EndsWith("</Pane>", StringComparison.OrdinalIgnoreCase))
                    nodeXML = clipboardText;
                else
                {
                    System.Console.Beep();
                    return;
                }
            }
            else
            {
                System.Console.Beep();
                return;
            }

            // Create a new pane document
            XmlDocument newPaneDoc = new XmlDocument();
            try
            {
                newPaneDoc.LoadXml(nodeXML);
            }
            catch (XmlException ex)
            {
                MessageBox.Show(ex.Message + "\n\n" + nodeXML, "Unable to paste node", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0);
                return;
            }

            // Append the node after the current selection
            XmlNode newPane;
            if (paneTree.SelectedNode == null || paneTree.SelectedNode == paneTree.Nodes[0])
            {
                newPane = wizard.DocumentElement.InsertAfter(wizard.ImportNode(newPaneDoc.DocumentElement, true), (XmlNode)paneTree.Nodes[0].Tag);
            }
            else
            {
                newPane = wizard.DocumentElement.InsertAfter(wizard.ImportNode(newPaneDoc.DocumentElement, true), (XmlNode)paneTree.SelectedNode.Tag);
            }

            // Add the new pane to the tree
            TreeNode newNode = new TreeNode(newPane.Attributes.GetNamedItem("id").Value);
            newNode.Tag = newPane;

            if (paneTree.SelectedNode == null || paneTree.SelectedNode == paneTree.Nodes[0])
                paneTree.Nodes[0].Nodes.Insert(0, newNode);
            else
                paneTree.Nodes[0].Nodes.Insert(paneTree.SelectedNode.Index + 1, newNode);

            changed = true;
        }

        #endregion Menu event handlers

        #endregion Event handlers

        #region Private methods

        /// <summary>
        /// The LoadCode method populates the "Settings" tab of the form with
        /// all custom statements, initialization and validation scripts, button
        /// label changes, etc.
        /// </summary>
        void LoadCode()
        {
            XmlNode selectedPane = (XmlNode)paneTree.SelectedNode.Tag;

            isDataReady = false;

            if (selectedPane.Name == "Global")
            {
                htmlCode.Text = "";
                htmlCode.Enabled = false;
            }
            else
            {
                htmlCode.Enabled = true;
                htmlCode.Text = selectedPane.SelectSingleNode("Body").ChildNodes[0].InnerText;
            }

            // Populate the list with custom statements, conditions, initialization, and validation nodes

            listDetail.Items.Clear();
            foreach (XmlNode customStatement in selectedPane.SelectNodes("CustomStatement|Condition|Initialization|Validation|ButtonNext|ButtonCancel"))
            {
                listDetail.Items.Add(customStatement);
            }
            if (listDetail.Items.Count > 0)
                listDetail.SelectedItem = 0;

            isDataReady = true;

        }

        /// <summary>
        /// The LoadPreview method takes the current pane's HTML, saves it to a file, and loads 
        /// that file into the preview pane's web control.
        /// </summary>
        void LoadPreview()
        {
            if (htmlCode.Enabled)
            {
                // Get the temporary directory to use
                String tempDir = System.Environment.GetEnvironmentVariable("TEMP", EnvironmentVariableTarget.Process);

                // Make sure the other required files are present in the same directory
                foreach (String requiredFile in new String[] {
                    "Wizard.hta", "Wizard.css", "WizUtility.vbs", "ZTIUtility.vbs", "ZTIConfigFile.vbs", "ZTIDataAccess.vbs", "ZTIBCDUtility.vbs", "ZTINicUtility.vbs", 
                    "Wizard.ico", "BackButton.JPG", "plusicon.gif", "minusico.gif", "SelectItem.jpg", "WelcomeBanner.jpg", "btnout.png", "btnover.png", "btnsel.png", "BackButton.png", "FolderIcon.png", "ItemIcon1.png", "MinusIcon1.png", "PlusIcon1.png", "SelectItem.png" })
                {
                    String filePath = "";
                    if (!File.Exists(tempDir + @"\" + requiredFile))
                    {
                        filePath = Utility.FindFile(definitionDirectory, requiredFile);
                        if (!String.IsNullOrEmpty(filePath))
                            File.Copy(filePath, tempDir + @"\" + requiredFile, true);
                    }
                }

                // Save a temporary copy of just this pane
                File.WriteAllText(tempDir + @"\Pane.htm", "<html><head><link href=\"Wizard.css\" type=\"text/css\" rel=\"stylesheet\"><body><div id=\"MyContentArea\" class=\"ContentArea\">" + htmlCode.Text + "</div></body></html>", Encoding.UTF8);

                // Show the pane in the HTML control
                webPreview.Navigate("file:" + tempDir + @"\Pane.htm");
            }
            else
            {
                webPreview.Navigate("about:blank");
            }
        }

        /// <summary>
        /// Configure the buttons appropriately, enabling them after a file has been
        /// selected and opened.
        /// </summary>
        void ConfigureButtons()
        {
            if (!String.IsNullOrEmpty(wizardDefinitionFile.Trim()))
            {
                btnAddPane.Enabled = true;
                btnRemovePane.Enabled = true;
                btnAdd.Enabled = true;
                btnRemove.Enabled = true;
                saveToolStripMenuItem.Enabled = true;
                saveAsToolStripMenuItem.Enabled = true;
                testToolStripMenuItem.Enabled = true;
            }
            else
            {
                btnAddPane.Enabled = false;
                btnRemovePane.Enabled = false;
                btnAdd.Enabled = false;
                btnRemove.Enabled = false;
                saveToolStripMenuItem.Enabled = false;
                saveAsToolStripMenuItem.Enabled = false;
                testToolStripMenuItem.Enabled = false;
            }
        }

        #endregion Private methods

        private void paneTree_DragEnter(object sender, DragEventArgs e)
        {
            e.Effect = DragDropEffects.Copy;
        }

        private void paneTree_DragDrop(object sender, DragEventArgs e)
        {
            TreeNode NewNode;

            if (e.Data.GetDataPresent("System.Windows.Forms.TreeNode", false))
            {
                Point pt = paneTree.PointToClient(new Point(e.X, e.Y));
                TreeNode DestinationNode = paneTree.GetNodeAt(pt);
                NewNode = (TreeNode)e.Data.GetData("System.Windows.Forms.TreeNode");
                if (DestinationNode.TreeView != NewNode.TreeView)
                {
                    DestinationNode.Nodes.Add((TreeNode)NewNode.Clone());
                    DestinationNode.Expand();
                }
            }

        }

    }
}
